-
Notifications
You must be signed in to change notification settings - Fork 357
Fix parsing issue for Flags enum values with combined flags #3324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
/AzurePipelines run |
|
No pipelines are associated with this pull request. |
|
/AzurePipelines run |
|
No pipelines are associated with this pull request. |
8e03a95 to
2bfab4c
Compare
|
/AzurePipelines run |
|
No pipelines are associated with this pull request. |
|
/AzurePipelines run |
| { | ||
| // Sort members by descending flag value to prioritize composite flags | ||
| var members = enumType.Members | ||
| .OrderByDescending(m => m.Value.Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrderBy is necessary? I cannot see any advantage to use the orderred members?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xuzhg Thanks for pointing this out.
The use of OrderByDescending is necessary for correct flag parsing when dealing with composite enum values. This ensures the output string uses the most specific (composite) flag names when possible. Removing the ordering would break this logic for flags enums with composite members.
Reasoning
-
Flags enums can have members that represent combinations of other members (composite flags). For example:
- Read = 1
- Write = 2
- ReadWrite = 3 (which is Read | Write)
-
If you process members in ascending order, you may match and consume the individual flags (Read, Write) before considering the composite flag (ReadWrite). This would prevent the composite flag from ever being matched.
-
By sorting in descending order, composite flags (with higher values) are considered first. This ensures that the largest possible flag combinations are matched before their individual components.
Example
Suppose value = 3 and members are:
- Read = 1
- Write = 2
- ReadWrite = 3
If you process in ascending order:
- Match Read (1), remaining = 2
- Match Write (2), remaining = 0
- ReadWrite (3) is never matched
If you process in descending order:
• Match ReadWrite (3), remaining = 0
• Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the 'Members' orderred always then we don't need to do order again and again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xuzhg This is possible. I have made the change here https://github.com/OData/odata.net/pull/3324/files#diff-bf7c2bf531a2d14a23a047276ee7fd40e4b2d6e5b0a1a2a550ba9c531163081d
|
/AzurePipelines run |
2 similar comments
|
/AzurePipelines run |
|
/AzurePipelines run |
db5c55c to
9384bc6
Compare
|
/AzurePipelines run |
…a/odata.net into fix/3244-support-flags-enum
| Assert.Equal((AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read), result.UserAccess); | ||
| Assert.Equal((AccessLevel.ReadWrite | AccessLevel.Execute), result.UserAccess); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why 2 assert statements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One is the composite of the other. For example:
// AccessLevel.ReadWrite is a composite of AccessLevel.Write | AccessLevel.Read
AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read == AccessLevel.ReadWrite | AccessLevel.Execute
// Where
AccessLevel.ReadWrite == AccessLevel.Write | AccessLevel.Read| Assert.Equal((AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read), enumResult[0]); | ||
| Assert.Equal((AccessLevel.ReadWrite | AccessLevel.Execute), enumResult[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why 2 assert statements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// <returns>A comma-separated string of flag names corresponding to the set bits in the specified value. Returns null otherwise.</returns> | ||
| public static string ParseFlagsFromIntegralValue(this IEdmEnumType enumType, long value) | ||
| { | ||
| var result = new List<string>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| var result = new List<string>(); | |
| List<string> result = new List<string>(); |
| var result = new List<string>(); | ||
| long remaining = value; | ||
|
|
||
| for (int index = enumType.Members.Count() - 1; index >= 0; index--) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling Members.Count() and Members.ElementAt(index) in a loop can turn this into an O(n^2) operation.
I'd suggest you first call .ToList():
List<IEdmEnumMember> members = enumType.Members.ToList();There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After doing the above, you can also avoid the string.Reverse call you're doing at the end by iterating the elements in the order you intend to have them in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppose value = 3 and members are:
- Read = 1
- Write = 2
- ReadWrite = 3
If you process in ascending order:
- Match Read (1), remaining = 2
- Match Write (2), remaining = 0
- ReadWrite (3) is never matched
If you process in descending order:
- Match ReadWrite (3), remaining = 0
- Done
In general, iterating in declaration order would allow us to avoid the Reverse call. However, for flag enums that include composite members (e.g., ReadWrite = 3), processing in ascending order would cause the individual flags (Read = 1, Write = 2) to consume the bits before the composite member is checked, so the composite would never match.
By iterating in descending order (from highest to lowest value), we ensure that composite members are matched first, which is necessary for correct flag decomposition. The Reverse at the end is then required to present the result in declaration order. This approach ensures correct handling for all flag enum scenarios, including those with composite members.
| { | ||
| IEdmEnumMember member = enumType.Members.ElementAt(index); | ||
| long flagValue = Convert.ToInt64(member.Value.Value); | ||
| if (flagValue != 0 && (remaining & flagValue) == flagValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the enum defines a zero-valued member (often "None"), will it not be excluded by this condition? Think about how that should be handled
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current implementation, a zero-valued member is not excluded. It is explicitly handled at the start of the method:
// Special handling for 0: return the name of the member whose value is 0 (e.g., "None"), if present.
// In Flags Enum, 0 typically represents no flags set.
if (value == 0)
{
return enumType.Members.FirstOrDefault(m => Convert.ToUInt64(m.Value.Value) == 0)?.Name;
}Basically, in a [Flags] enum, 0 conventionally means "no flags set" and is not a bitwise flag.
We cannot handle 0-valued members inside the main loop because 0 does not represent any set bit in a flags value. Including 0 in the loop would cause it to match any value (since (remaining & 0) == 0 is always true), which is incorrect. Therefore, 0-valued members must be handled separately before the loop to ensure correct and conventional behavior.
| public static string ParseFlagsFromIntegralValue(this IEdmEnumType enumType, long value) | ||
| { | ||
| var result = new List<string>(); | ||
| long remaining = value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should opt for ulong here to keep bitwise match clean and predictable even if a value is negative (not sure if OData supports use of enum members with negative integral values)
unchecked
{
ulong remaining = (ulong)value;
// ...
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have udpated it to use ulong:
ulong remaining = unchecked((ulong)value);|
/AzurePipelines run |
Issues
This pull request fixes #3244, fixes #3226, fixes #3178
Description
OData queries fail when filtering against Flags enum properties using combined enum values, whether specified as string representations (
'Premium,Loyal') or numeric equivalents (66).The current implementation in
MetadataBindingUtils.csdoes not properly handle:Changes Made
Query Patterns Now Supported
OData ABNF rules for enums
https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt

Rule 1: Enum literals follow the format
qualifiedEnumTypeName'SQUOTE enumValue SQUOTE.qualifiedEnumTypeName(e.g.,Namespace.Color) is optional.Namespace.Color'Blue'or'Blue'.Rule 2:
enumValuecan be a single value or multiple comma-separated values (for flags).'Blue','Red,Green'.Rule 3: Each
singleEnumValuecan be a named member (Red) or a numeric value (4).Rule 4: Numeric values must be valid
int64.'8'or8.Checklist (Uncheck if it is not completed)
Additional work necessary
If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.
Repository notes
Team members can start a CI build by adding a comment with the text
/AzurePipelines runto a PR. A bot may respond indicating that there is no pipeline associated with the pull request. This can be ignored if the build is triggered.Team members should not trigger a build this way for pull requests coming from forked repositories. They should instead trigger the build manually by setting the "branch" to
refs/pull/{prId}/mergewhere{prId}is the ID of the PR.